package ua.com.datastorm.eventstore.orientdb;

import com.orientechnologies.orient.core.db.document.ODatabaseDocument;
import com.orientechnologies.orient.core.metadata.schema.OClass;
import com.orientechnologies.orient.core.metadata.schema.OProperty;
import com.orientechnologies.orient.core.metadata.schema.OSchema;
import com.orientechnologies.orient.core.metadata.schema.OType;
import com.orientechnologies.orient.core.record.impl.ODocument;
import org.axonframework.domain.AggregateIdentifier;
import org.axonframework.domain.DomainEvent;
import org.axonframework.eventstore.EventSerializer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Presentation of OrientDb document that will contain {@link DomainEvent} data and also metadata that will
 * be used in queries to find Domain Event of given type.
 * Instance of given document can be created by calling of {@link #asDocument(ODatabaseDocument)} method.
 * <p/>
 * Document will contain following fields:
 * <ol>
 * <li>{@link #AGGREGATE_IDENTIFIER_FIELD}</li>
 * <li>{@link #AGGREGATE_TYPE_FIELD}</li>
 * <li>{@link #SEQUENCE_NUMBER_FIELD}</li>
 * <li>{@link #TIMESTAMP_FIELD}</li>
 * <li>{@link #BODY_FIELD}</li>
 * </ol>
 * <p/>
 * Document will have class named {@link #DOMAIN_EVENT_CLASS}.
 *
 * @author Andrey Lomakin
 */
class DomainEventEntry {
    private static final Logger logger = LoggerFactory.getLogger(DomainEventEntry.class);

    /**
     * Name of the document class that will be used to store Domain Events.
     */
    static final String DOMAIN_EVENT_CLASS = "DomainEvent";

    /**
     * Name of the field that will contain String presentation of {@link AggregateIdentifier}
     * of the related Aggregate.
     * <p/>
     * OrientDb type : {@link OType#STRING}. Mandatory. Not Null.
     */
    static final String AGGREGATE_IDENTIFIER_FIELD = "aggregateIdentifier";

    /**
     * Name of the field that will contain name of the related aggregate type.
     * <p/>
     * OrientDb type : {@link OType#STRING}. Mandatory. Not Null.
     */
    static final String AGGREGATE_TYPE_FIELD = "aggregateType";

    /**
     * Name of the field that will contain sequence number of the stored Domain Event.
     * <p/>
     * OrientDb type : {@link OType#LONG}. Mandatory. Not Null.
     */
    static final String SEQUENCE_NUMBER_FIELD = "sequenceNumber";

    /**
     * Name of the field that will contain time stamp (yyyy-MM-ddTHH:mm:ss.SSSZZ)
     * String presentation  of the stored Domain Event.
     * <p/>
     * OrientDb type : {@link OType#STRING}. Mandatory. Not Null. Max And Min length equals to 29.
     */
    static final String TIMESTAMP_FIELD = "timestamp";

    /**
     * Name of the field that will contain generated by {@link EventSerializer} byte presentation
     * of the stored Domain Event.
     * <p/>
     * OrientDb type : {@link OType#BINARY}. Mandatory. Not Null.
     */
    static final String BODY_FIELD = "body";

    private final EventSerializer eventSerializer;
    private final DomainEvent event;
    private AggregateIdentifier aggregateIdentifier;
    private String aggregateType;

    /**
     * @param aggregateType   Type of the related aggregate.
     * @param event           DomainEvent to be stored.
     * @param eventSerializer {@link EventSerializer} that must be used to convert Domain Event to the
     *                        binary presentation.
     */
    DomainEventEntry(String aggregateType, DomainEvent event, EventSerializer eventSerializer) {
        this.aggregateType = aggregateType;
        this.aggregateIdentifier = event.getAggregateIdentifier();
        this.event = event;
        this.eventSerializer = eventSerializer;
    }

    /**
     * @return Domain Event to be stored.
     */
    DomainEvent getEvent() {
        return event;
    }

    /**
     * @return Related Aggregate type.
     */
    String getAggregateType() {
        return aggregateType;
    }

    /**
     * Stores {@link DomainEvent} to the newly created document.
     * If class related to the given document does not exist it will be created and bounded
     * to the passed in cluster name.
     * If class exist but not bounded to the passed in cluster it will be.
     * <p/>
     * Created Document is not stored, if you need to store document call document.save()
     * and schema.save() to persist all changes.
     *
     * @param databaseDocument Current database instance.
     * @return Document presentation of Domain Event.
     */
    ODocument asDocument(ODatabaseDocument databaseDocument) {
        final OClass eventClass = createClass(databaseDocument);

        final ODocument eventDocument = new ODocument(eventClass);
        eventDocument.field(AGGREGATE_IDENTIFIER_FIELD, aggregateIdentifier.asString());
        eventDocument.field(SEQUENCE_NUMBER_FIELD, event.getSequenceNumber());
        eventDocument.field(TIMESTAMP_FIELD, event.getTimestamp().toString());
        eventDocument.field(BODY_FIELD, eventSerializer.serialize(event));
        eventDocument.field(AGGREGATE_TYPE_FIELD, aggregateType);

        return eventDocument;
    }

    /**
     * Creates document class definition for the Domain Event type that is presented by given document.
     * All descendants should override this method to provide its own class definition.
     *
     * @param databaseDocument Current database instance.
     * @return Document class that presents Domain Event and auxiliary metadata.
     */
    protected OClass createClass(ODatabaseDocument databaseDocument) {
        final OSchema schema = databaseDocument.getMetadata().getSchema();
        OClass eventClass = schema.getClass(DOMAIN_EVENT_CLASS);

        if (eventClass != null) {
            return eventClass;
        }


        logger.debug("OClass \"{}\" was created.", DOMAIN_EVENT_CLASS);

        eventClass = schema.createClass(DOMAIN_EVENT_CLASS);

        eventClass.createProperty(AGGREGATE_IDENTIFIER_FIELD, OType.STRING).setMandatory(true).setNotNull(true).
                createIndex(OProperty.INDEX_TYPE.NOTUNIQUE);
        eventClass.createProperty(SEQUENCE_NUMBER_FIELD, OType.LONG).setMandatory(true).setNotNull(true).
                createIndex(OProperty.INDEX_TYPE.NOTUNIQUE);
        eventClass.createProperty(TIMESTAMP_FIELD, OType.STRING).setMin("29").setMax("29").setMandatory(true).
                setNotNull(true);
        eventClass.createProperty(BODY_FIELD, OType.BINARY).setMandatory(true).setNotNull(true);
        eventClass.createProperty(AGGREGATE_TYPE_FIELD, OType.STRING).setMandatory(true).setNotNull(true).
                createIndex(OProperty.INDEX_TYPE.NOTUNIQUE);

        return eventClass;
    }

}
